InvariantMonoidalって何?
はじめに
ScalaMatsuri 2022で拝見したとても興味深いセッションで「HKD[T, F]
からF[T]
を得るにはFがInvariantMonoidal
である必要がある」という制約が出てきて、InvariantMonoidalが何なのか気になったのでCatsのドキュメントを読みながら手を動かして確認してみました。
前準備
この記事ではSemigroupを使っていくのでコンストラクタとLongに対するインスタンスを定義しておきます。
import cats._ import java.util.Date trait SemigroupExample: def semigroup[A](f: (x:A, y:A) => A): Semigroup[A] = (x: A, y: A) => f(x,y) val longToDate: Long => Date = new Date(_) val dateToLong: Date => Long = _.getTime given semigroupLong: Semigroup[Long] = semigroup(_+_)
InvariantMonoidalはInvariantでありSemigroupal
まずドキュメントを見るとInvariantMonoidalはInvariantでありSemigroupalであることがわかります。
InvariantMonoidal
combinesInvariant
andSemigroupal
with the addition of aunit
methods, defined in isolation theInvariantMonoidal
シグネチャは以下の通りなのですがそれぞれみていきます。
trait InvariantMonoidal[F[_]] { def unit: F[Unit] def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] }
Invariant
Invariantのシグネチャは以下の通りです。Covariant FunctorそしてContravariant Functorを悪魔合体したような見た目です。
def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B]
今、準備しておいたSemigroup[Long]を使ってSemigroup[Date]を作ることを考えます。もしSemigroupがCovariant Functorを形成する場合、Semigroup[Date]を作れるでしょうか?
おそらく以下のようなコードになりますが、combineしたBをAに戻す方法がないので手詰まりです。
given covariantSemigroup: Functor[Semigroup] = new Functor[Semigroup] { override def map[A, B](fa: Semigroup[A])(f: A => B): Semigroup[B] = semigroup[B]((x: A, y: A) => fa.combine(f(x), f(y))) }
Contravariantの場合も同様です。
given contravariantSemigroup: Contravariant[Semigroup] = new Contravariant[Semigroup] { override def contramap[A, B](fa: Semigroup[A])(f: B => A): Semigroup[B] = semigroup((x:B, y:B) => fa.combine(f(x), f(y))) }
Invariantならできます。
// invariantは定義できる given invariantSemigroup: Invariant[Semigroup] = new Invariant[Semigroup]: override def imap[A, B](fa: Semigroup[A])(f: A => B)(g: B => A): Semigroup[B] = semigroup[B]((x:B, y:B) => f(fa.combine(g(x), g(y))))
そしてSemigroup[Date]も!!
//invariantでsemigroupを導けた!! given semigroupDate: Semigroup[Date] = summon[Invariant[Semigroup]].imap(summon[Semigroup[Long]])(longToDate)(dateToLong)
Semigroupal
Semigroupal のシグネチャは以下です。SemigroupalではF[A], F[B]をF[(A, B)]に写します。
def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
定義をそのまま述べるのに意味があるのか疑問が表面化する前に実装してみます。
given semigroupalSemigroup: Semigroupal[Semigroup] = new Semigroupal[Semigroup]: override def product[A, B](fa: Semigroup[A], fb: Semigroup[B]): Semigroup[(A, B)] = semigroup { case ((a1, b1), (a2, b2)) => (fa.combine(a1, a2), fb.combine(b2, b2))
ふたたび InvariantMonoidal
準備ができたのでInvariantMonoidalを実装します。
// semigroupに対するinvariant monoidal given invariantMonoidalSemigroup: InvariantMonoidal[Semigroup] = new InvariantMonoidal[Semigroup]: override def unit: Semigroup[Unit] = semigroup((_, _) => ()) override def product[A, B]( fa: Semigroup[A], fb: Semigroup[B] ): Semigroup[(A, B)] = summon[Semigroupal[Semigroup]].product(fa, fb) override def imap[A, B](fa: Semigroup[A])(f: A => B)(g: B => A): Semigroup[B] = summon[Invariant[Semigroup]].imap(fa)(f)(g)
これを使うと既にSemigroupを定義済みの型からなるcase classに対してSemigroupを導けます。
final case class Foo(long: Long, date: Date) object Foo: def unapply(foo: Foo): Option[(Long, Date)] = Some(foo.long, foo.date) //invariant monoidal を使ってSemigroupを導く val semigroupFoo: Semigroup[Foo] = invariantMonoidalSemigroup.imap( invariantMonoidalSemigroup.product(summon[Semigroup[Long]], summon[Semigroup[Date]]) )(Foo.apply.tupled)(Foo.unapply.unlift)
別の見方をするとInvariantMonoidalはInvariantの一般系といえます。imap2...imap N-1を使ってimap3...imap Nを導けます。
extension [F[_]](im: InvariantMonoidal[F]) def imap2[A, B, C](f: ((A, B)) => C)(g: C => (A, B))(fa: F[A], fb: F[B]): F[C] = im.imap(im.product(fa, fb))(f)(g) def imap3[A, B, C, D](f: ((A, B, C)) => D)(g: D => (A, B, C))(fa: F[A], fb: F[B], fc:F[C]):F[D] = imap2[A, (B, C), D] (f compose { case (a, (b, c))=> (a,b,c)}) (g andThen { case (a, b, c) => (a, (b, c))}) (fa, imap2[B, C, (B, C)](identity)(identity)(fb, fc))
まとめ
- InvariantMonoidalはInvariantでありSemigroupalである
- InvariantMonoidalはInvariantの一般系ともみなせる